using System;
using System.Collections.Generic;
using System.IO;

namespace Abide_Halo_Library.IO
{
    public class HaloIO : MarshalByRefObject, IDisposable
    {
        /// <summary>
        /// Gets the current Endianness used by this I/O instance.
        /// </summary>
        public Endianness Endianness
        {
            get { return endianness; }
            set { endianness = value; SetEndianness(); }
        }
        /// <summary>
        /// Gets and returns the location of the map file on the HDD.
        /// </summary>
        public String FileLocation
        {
            get { return fileLocation; }
        }
        /// <summary>
        /// Gets and returns a BinaryReader used to read data from the halo map.
        /// </summary>
        public BinaryReader In
        {
            get { return _in; }
        }
        /// <summary>
        /// Gets and returns a BinaryReader used to write data to the halo map.
        /// </summary>
        public BinaryWriter Out
        {
            get { return _out; }
        }
        /// <summary>
        /// Gets and returns a HaloStream reference used to handle the file data's I/O.
        /// </summary>
        public HaloStream MapStream
        {
            get { return _mapStream; }
        }
        /// <summary>
        /// Sets the position to begin reading or writing the map file.
        /// </summary>
        public Int64 Position
        {
            get { return _mapStream.Position; }
            set { _mapStream.Position = value; }
        }
        /// <summary>
        /// Gets and returns true if the current IO instance is open and ready to read or write.
        /// </summary>
        public bool Open
        {
            get { if (_mapStream == null) return false; else return _mapStream.CanRead & _mapStream.CanWrite & _mapStream.CanSeek; }
        }

        internal List<MapEdit> WriteHistory = null;
        Endianness endianness = Endianness.Little_Endian;
        String fileLocation = string.Empty;
        BinaryReader _in = null;
        BinaryWriter _out = null;
        HaloStream _mapStream = null;

        public HaloIO(string fileLocation)
        {
            if (File.Exists(fileLocation))
                this.fileLocation = fileLocation;
            else throw new ArgumentException("File does not exist.", "fileLocation");
            WriteHistory = new List<MapEdit>();
        }
        public HaloIO(string fileLocation, Endianness endianness)
        {
            if (File.Exists(fileLocation))
                this.fileLocation = fileLocation;
            else throw new ArgumentException("File does not exist.", "fileLocation");
            this.endianness = endianness;
            WriteHistory = new List<MapEdit>();
        }
        public HaloIO(Stream mapStream)
        {
            this.fileLocation = string.Empty;
            WriteHistory = new List<MapEdit>();
        }
        public HaloIO(Stream mapStream, Endianness endianness)
        {
            this.fileLocation = string.Empty;
            this.endianness = endianness;
            WriteHistory = new List<MapEdit>();
        }

        public void Dispose()
        {
            _mapStream.Dispose();
            _in.Dispose();
            _out.Dispose();
        }
        public void OpenIO()
        {
            if (!Open)
            {
                _mapStream = new HaloStream(fileLocation, this);
                _in = new BinaryReader(_mapStream);
                _out = new BinaryWriter(_mapStream);
            }
        }
        public void CloseIO()
        {
            _out.Close();
            _in.Close();
            _mapStream.Close();
        }
        public Stream CreateMap()
        {
            Stream stream = null;
            try { stream = new FileStream(fileLocation, FileMode.Create); }
            catch (IOException) { stream = new MemoryStream(); }
            return stream;
        }
        public MapEdit[] GetHistory()
        {
            return WriteHistory.ToArray();
        }

        private void SetEndianness()
        {
            if (_mapStream.CanRead || _mapStream.CanWrite)
                _mapStream.endianness = Endianness;
        }
    }
    public class HaloStream : Stream
    {
        HaloIO owner = null;
		Stream innerStream = null;
        internal Endianness endianness;

        public HaloStream(string fileLocation, HaloIO owner)
        {
            this.owner = owner;
            this.endianness = owner.Endianness;
            this.innerStream = new FileStream(fileLocation, FileMode.Open);
        }
        public HaloStream(Stream mapStream, HaloIO owner)
        {
            this.owner = owner;
            this.endianness = owner.Endianness;
            this.innerStream = mapStream;
        }
        public override Boolean CanRead
        {
            get { return innerStream.CanRead; }
        }
        public override Boolean CanSeek
        {
            get { return innerStream.CanSeek; }
        }
        public override Boolean CanWrite
        {
            get { return innerStream.CanWrite; }
        }
        public override void Flush()
        {
            innerStream.Flush();
        }
        public override Int64 Length
        {
            get { return innerStream.Length; }
        }
        public override Int64 Position
        {
            get
            {
                return innerStream.Position;
            }
            set
            {
                innerStream.Position = value;
            }
        }
        public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
        {
            //Read
            Int32 value = innerStream.Read(buffer, offset, count);

            //Reverse?
            if (endianness == Endianness.Big_Endian)
                Array.Reverse(buffer);

            //Return
            return value;
        }
        public override Int64 Seek(Int64 offset, SeekOrigin origin)
        {
            return innerStream.Seek(offset, origin);
        }
        public override void SetLength(Int64 value)
        {
            innerStream.SetLength(value);
        }
        public override void Write(Byte[] buffer, Int32 offset, Int32 count)
        {
            //Get data
            MapEdit Edit = new MapEdit();
            Edit.offset = Position;
            Edit.length = count;
            Edit.edit = new Byte[count];
            Edit.original = new Byte[count];
            Array.Copy(buffer, offset, Edit.edit, 0, count);
            Read(Edit.original, offset, count);
            Position = Edit.offset;

            //Add
            if(!System.Linq.Enumerable.SequenceEqual(Edit.original, Edit.edit))
                owner.WriteHistory.Add(Edit);

            //Reverse?
            if (endianness == Endianness.Big_Endian)
                Array.Reverse(buffer);

            //Write
            innerStream.Write(buffer, offset, count);
        }
        public override void Close()
        {
            base.Close();
            innerStream.Close();
        }
        protected override void Dispose(Boolean disposing)
        {
            innerStream.Close();
            innerStream.Dispose();
            base.Dispose(disposing);
        }
    }
    public enum Endianness
    {
        Big_Endian,
        Little_Endian
    }
    public struct MapEdit
    {
        public Int32 length;
        public Int64 offset;
        public Byte[] original;
        public Byte[] edit;
    }
    public static class IOExtensions
    {
        /// <summary>
        /// Writes a non-lengthed prefix ASCII-Encoded string to the underlying stream.
        /// </summary>
        /// <param name="Out">The System.IO.BinaryWriter instance.</param>
        /// <param name="text">The string to be written</param>
        public static void WriteASCII(this BinaryWriter Out, string text)
        {
            System.Text.ASCIIEncoding Encoding = new System.Text.ASCIIEncoding();
            Out.Write(Encoding.GetBytes(text));
        }
        /// <summary>
        /// Reads an ASCII-Encoded string from the underlying stream.
        /// </summary>
        /// <param name="In">The System.IO.BinaryReader instance.</param>
        /// <param name="length">The length of the data to be read.</param>
        /// <returns>The ASCII-Encoded string.</returns>
        public static string ReadASCII(this BinaryReader In, int length)
        {
            System.Text.ASCIIEncoding Encoding = new System.Text.ASCIIEncoding();
            byte[] data = In.ReadBytes(length);
            return new string(Encoding.GetChars(data));
        }
        /// <summary>
        /// Writes a non-lengthed prefix Unicode-Encoded string to the underlying stream.
        /// </summary>
        /// <param name="Out">The System.IO.BinaryWriter instance.</param>
        /// <param name="text">The string to be written</param>
        public static void WriteUnicode(this BinaryWriter Out, string text)
        {
            System.Text.UnicodeEncoding Encoding = new System.Text.UnicodeEncoding();
            Out.Write(Encoding.GetBytes(text));
        }
        /// <summary>
        /// Reads an Unicode-Encoded string from the underlying stream.
        /// </summary>
        /// <param name="In">The System.IO.BinaryReader instance.</param>
        /// <param name="length">The length of the data to be read.</param>
        /// <returns>The Unicode-Encoded string.</returns>
        public static string ReadUnicode(this BinaryReader In, int length)
        {
            System.Text.UnicodeEncoding Encoding = new System.Text.UnicodeEncoding();
            byte[] data = In.ReadBytes(length);
            return new string(Encoding.GetChars(data));
        }
    }
}

